/*
 * Copyright (c) 2014, Hsu Shih-Chia (schsu01@gmail.com)
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qood;

import java.util.*;

/**
 * QView is similar to <a href="http://en.wikipedia.org/wiki/View_(SQL)">View</a> in database.
 * <p/>
 * <p>It describes how a QSQL query QModel data, and about to be executed into another QModel as result.
 *
 * @author SCHsu01
 * @version 1.0
 */
public interface QView extends QSQL {
  QView distinct(boolean unique);

  QView select(String... formulas);

  QView selectAs(String alias, String formula);

  QView selectAs(String[] alias, String... formulas);

  QView from(String name);

  QView where(QWhere where);

  QView hashJoin(String name, boolean innerJoin, String[] mainFormula, String... matchFormula);

  QView hashJoin(String name, boolean innerJoin, String... matchFormula);

  QView groupBy(String... formulas);

  QView having(QWhere where);

  QView orderBy(String... formulas);

  QView orderBy(String formula, boolean desc);

  QView orderBy(String[] formulas, boolean... desc);

  QView clear(boolean select, boolean join, boolean groupBy, boolean orderBy);

  /**
   * Set a QModel data for {@link #from(java.lang.String)} or {@link #hashJoin(String, boolean, String...)}
   *
   * @param name  alias name used in FROM or JOIN parts
   * @param model actual data
   * @return a reference to this Object
   */
  QView with(String name, QModel model);

  /**
   * Execute query(QSQL) with given data(QModel) into another QModel as result
   *
   * @return executed result
   */
  QModel result();
}

final class View implements QView {
  private final Map<String, Model> source;
  private final SQL sql;
  private transient Model result;

  View(Map<String, Model> source, SQL sql) {
    this.source = new LinkedHashMap<String, Model>(source);
    this.sql = sql;
  }

  private View changed() {
    result = null;
    return this;
  }

  SQL getSQL() {
    return sql;
  }

  public Model result() {
    return null == result ? result = sql.result(this) : result;
  }

  public View with(String name, QModel model) {
    if (null == name) throw new IllegalArgumentException("name is required for QView.with(name, model)");
    source.put(name, (Model) model);
    return changed();
  }

  Map<String, Model> getSource() {//key feature
    return source;
  }

  public View clear(boolean select, boolean join, boolean groupBy, boolean orderBy) {
    sql.clear(select, join, groupBy, orderBy);
    return changed();
  }

  public View distinct(boolean unique) {
    sql.distinct(unique);
    return changed();
  }

  public View from(String name) {
    sql.from(name);
    return changed();
  }

  public View hashJoin(String name, boolean innerJoin, String... matchFormula) {
    sql.hashJoin(name, innerJoin, matchFormula);
    return changed();
  }

  public View hashJoin(String name, boolean innerJoin, String[] mainFormula, String... matchFormula) {
    sql.hashJoin(name, innerJoin, mainFormula, matchFormula);
    return changed();
  }

  public View groupBy(String[] formulas) {
    sql.groupBy(formulas);
    return changed();
  }

  public View having(QWhere where) {
    sql.having(where);
    return changed();
  }

  public View orderBy(String formula, boolean desc) {
    sql.orderBy(formula, desc);
    return changed();
  }

  public View orderBy(String... formulas) {
    sql.orderBy(formulas);
    return changed();
  }

  public View orderBy(String[] formulas, boolean... desc) {
    sql.orderBy(formulas, desc);
    return changed();
  }

  public View selectAs(String alias, String formula) {
    sql.selectAs(alias, formula);
    return changed();
  }

  public View selectAs(String[] alias, String... formulas) {
    sql.selectAs(alias, formulas);
    return changed();
  }

  public View select(String... formulas) {
    sql.select(formulas);
    return changed();
  }

  public View where(QWhere where) {
    sql.where(where);
    return changed();
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
    boolean join = false;
    for (String name : sql.usedSource()) {
      QModel model = source.get(name);
      if (join) sb.append(", <");
      else {//from part
        if (null == name) name = StringUtil.EMPTY_STR;
        if (null == model) model = source.get(null);
        sb.append("\nWITH <");
        join = true;
      }
      sb.append(name);
      if (null == model) sb.append("> AS (\n ? //.with(\"").append(name).append("\",...) required!");
      else sb.append("> AS (\n ").append(Arrays.asList(model.getColNames())).append(" * ").append(model.getRowCount());
      sb.append("\n)");
    }
    sb.append(' ').append(sql);
    return sb.toString();
  }
}

@SuppressWarnings({"NullableProblems"})
final class Temp {
  final View view;
  final Model main;
  private final Map<String, Model> source;
  private final MultiMap<String, String> colSource = new MultiMap<String, String>(new TreeMap<String, Collection<String>>(), true);
  private final Map<String, Map<String, Col>> sourceCol = new HashMap<String, Map<String, Col>>();
  private final Map<String, Col> virtualCol = new HashMap<String, Col>(), groupAggreg = new HashMap<String, Col>();
  private Map<String, Col> groupCols, topCol;
  private IntRead[] groupRows;

  Temp(View view, String selectFrom) {
    this.view = view;
    this.source = view.getSource();
    Model srcMain = getSource(selectFrom);
    if (null == srcMain && null != selectFrom) srcMain = getSource(null);
    if (null == srcMain) throw needSource(selectFrom);
    this.main = srcMain;
  }

  private RuntimeException needSource(String name) {
    return new IllegalStateException("select from <" + name + "> not found in " + view);
  }

  Map<String, Col> initCols(String alias, Model model,/*nullable*/ IntList joinedRow) {
    final String aliasDot = alias + ".";
    final String[] names = model.getColNames();
    final Col[] cols = null == joinedRow ? model.getCols() : Col.getPartial(joinedRow.toRead(), model.getCols());
    final Map<String, Col> colMap = new HashMap<String, Col>();
    for (int i = 0, colCnt = model.getColCount(); i < colCnt; i++) {
      final String name = names[i];
      final Col col = cols[i];
      colMap.put(name, col);
      if (null != alias) virtualCol.put(aliasDot + name, col);
    }
    sourceCol.put(alias, colMap);//no virtual Cols
    return colMap;
  }

  void initCols(String alias, Model model) {
    for (final String name : initCols(alias, model, null).keySet())
      colSource.put(name, alias);//no virtual Cols
  }

  void setGrouped(String[] groupNames, Col[] groupCols, IntRead[] groupRows) {
    topCol = new HashMap<String, Col>();
    virtualCol.put("*", Col.getConst(String.class, null));//special rule for COUNT(*)
    if (null == groupRows) return;//no group-by
    final Map<String, Col> nameCols = new HashMap<String, Col>();
    for (int i = 0, n = groupNames.length; i < n; i++) nameCols.put(groupNames[i], groupCols[i]);
    this.groupCols = nameCols;
    this.groupRows = groupRows;
  }

  public Col[] getCols(String... formula) {//key feature
    if (StringUtil.isEmpty(formula)) return null;
    final int n = formula.length;
    final Col[] result = new Col[n];
    for (int i = 0; i < n; i++) result[i] = getCol(formula[i]);
    return result;
  }

  public Col getCol(String formula) {//1.source-unknown formula 2.aggregated formula 3.error
    final Col found = findOrParse(null, formula = formula.trim());
    if (!(null == topCol || found.isConstant())) topCol.put(formula, found);
    return found;
  }

  Col[] getCols(String[] alias, String[] formula) {
    final int n = formula.length;
    final Col[] result = new Col[n];
    for (int i = 0; i < n; i++) {
      final String f = formula[i], a = alias[i];
      final Col c = result[i] = getCol(f);
      if (!f.equals(a)) virtualCol.put(a, c);//for 循環引用alias
    }
    return result;
  }

  private Col getCache(Map<String, Col> cache, String formula) {//get existed, no parsing
    Col result;
    if (null == cache) {
      if (!((null == groupCols || null == (result = groupCols.get(formula))) && //get from already parsed
        null == (result = virtualCol.get(formula)) && null == (result = groupAggreg.get(formula)))) return result;
      final Collection<String> sources = colSource.get(formula);//search for column name
      return null == sources ? null : sourceCol.get(sources.iterator().next()).get(formula);//ambiguous column => get first;
    }
    if (null == (result = cache.get(formula))) result = virtualCol.get(formula);//for join
    return result;//throw new IllegalStateException(formula + " not found in " + view);
  }

  private Col setCache(Map<String, Col> cache, String formula, Col result) {//save result to cache
    (null == cache ? virtualCol : cache).put(formula, result);
    return result;
  }

  Col[] getJoinCols(String source, String... formula) {//for join only
    if (StringUtil.isEmpty(formula)) return null;
    final Map<String, Col> colMap = sourceCol.get(source);
    final int n = formula.length;
    final Col[] result = new Col[n];
    for (int i = 0; i < n; i++) result[i] = findOrParse(colMap, formula[i].trim());
    return result;
  }

  private Col findOrParse(Map<String, Col> cache, String trimedFormula) {//get existed, or parsing
    final Col existed = getCache(cache, trimedFormula);//if not existed, parse it
    if (null != existed) return existed;
    final Deque<Col> result = new ArrayDeque<Col>();
    parseOperator(cache, result, trimedFormula, 0, trimedFormula.length());//will cache internally
    return result.pollLast();
  }

  private int parseOperator(Map<String, Col> cache, Deque<Col> output, String s, int min, int max) {
    final Deque<Operator> ops = new ArrayDeque<Operator>();
    final Deque<Col> temp = new ArrayDeque<Col>();
    int pSp = min;
    for (int i = min; i < max; i++) {
      final char c = s.charAt(i);
      final Operator op = Operator.getOperator(c, s, i);
      if (null != op) {
        parseVariable(cache, temp, s.substring(pSp, i));
        pSp = i = op.next(ops, temp, i);
      } else if (')' == c || ',' == c) {//finish
        parseResult(s.substring(min, i), cache, temp, ops, output, pSp - min);
        return i;
      } else if ('(' == c) //formula or quote
        pSp = 1 + (i = parseFormula(cache, temp, s, pSp, i, max));
      else if ('\'' == c)//string
        pSp = 1 + (i = parseString(temp, s, i + 1, max));
    }
    parseResult(s, cache, temp, ops, output, pSp - min);//sorry for getCache again
    return max;
  }

  private void parseResult(String s, Map<String, Col> cache, Deque<Col> temp, Deque<Operator> ops, Deque<Col> output, int var) {
    final String formula = s.trim();
    final Col existed = getCache(cache, formula);//if not existed, save cache
    if (null == existed) {
      parseVariable(cache, temp, s.substring(var));
      if (ops.isEmpty()) {//no operator found
        if (temp.isEmpty()) return;//empty function
      } else {//have operator, merge & cache required
        while (!ops.isEmpty()) ops.removeLast().merge(temp);
        setCache(cache, formula, temp.getLast());
      }
      output.add(temp.pollLast());
    } else output.add(existed);
  }

  private void parseVariable(Map<String, Col> cache, Deque<Col> output, String s) {//never be formula, string... etc
    if (0 == (s = s.trim()).length()) return;
    final Col existed = getCache(cache, s);//if not existed, save cache
    if (null == existed) {//if not existed, must be number and save cache
      final double d = TypeUtil.toDouble(s);
      if (Double.isNaN(d)) throw new IllegalArgumentException("variable not found: " + s + " in " + view);
      output.add(setCache(virtualCol, s, Col.getConst(Double.class, d)));//cache new number const
    } else output.add(existed);
  }

  private int parseFormula(Map<String, Col> cache, Deque<Col> output, String s, int start, int quote, int max) {
    final String func = s.substring(start, quote).trim();
    final Formula fn;
    if (0 == func.length()) fn = null;
    else {
      fn = Formula.getInstance(func);
      if (null == fn) throw new UnsupportedOperationException("Undefined function:\"" + func + "\" in " + view);
    }
    final Deque<Col> params = new ArrayDeque<Col>();
    for (int i = quote + 1; i < max; i++)
      if (')' == s.charAt(i = parseOperator(cache, params, s, i, max))) {//formula closed
        if (null == fn)//parenthesis only, already cached in parseOperator
          if (params.isEmpty()) throw new IllegalArgumentException("empty () in " + view);
          else output.add(params.pollLast());
        else {//in formula
          final String formula = s.substring(start, i + 1);
          final Col existed = getCache(cache, formula);//if not existed, save cache
          if (null == existed) {
            if (fn.isAggregate()) {
              if (null == cache) cache = groupAggreg;//save cache to groupAggreg
              else throw new UnsupportedOperationException("join by aggregation in " + view);//never be join
              for (final Col col : params)
                if (col.isAggregate())
                  throw new UnsupportedOperationException("Nested Aggregation:\"" + formula + "\" in " + view);
            }
            output.add(setCache(cache, formula, fn.createCol(params.toArray(new Col[params.size()]))));
          } else output.add(existed);
        }
        return i;
      }
    throw new IllegalStateException("function:\"" + func + "\" not closed in " + view);
  }

  private int parseString(Deque<Col> output, String s, int min, int max) {//in string
    boolean replace = false;
    for (int i = min; i < max; i++) {
      final char c = s.charAt(i);
      if ('\'' == c) {
        final int next = i + 1;
        if (max == next || '\'' != s.charAt(next)) {//string closed
          final String formula = s.substring(min - 1, i + 1), val = s.substring(min, i);
          final Col existed = getCache(null, formula);//if not existed, save cache
          output.add(null == existed ? setCache(virtualCol, formula, Col.getConst(
            String.class, replace ? val.replaceAll("''", "'") : val)) : existed);
          return i;
        } else {
          replace = true;
          i += 2;
        }
      }
    }
    throw new IllegalStateException("String not closed: " + s.substring(min, max) + " in " + view);
  }

  Model getSource(/*nullable*/String name) {
    return source.get(name);
  }

  String[] getDefaultCols() {
    return null == groupCols ? main.getColNames() : StringUtil.toArray(groupCols.keySet());
  }

  @SuppressWarnings({"SuspiciousToArrayCall", "unchecked"})
  IntRead doSummary(IntRead rows, Col[] select) {
    final int aggregCnt = groupAggreg.size();//todo: let "select X, X*Y group by X" work
    final boolean noGroupby = null == groupCols, noAggreg = 0 == aggregCnt;
    if (noGroupby && noAggreg) return rows;//normal query, won't check
    for (final Map.Entry<String, Col> entry : topCol.entrySet())//check if all non-agg cols in groupCols
      if (!entry.getValue().isAggregate() && (noGroupby ||//todo: fix "group X order by INT(X)" error
        /*known bug: groupCols.containsKey not handle formula*/!groupCols.containsKey(entry.getKey())))
        throw new IllegalStateException("not group-by \"" + entry.getKey() + "\" in " + view);
    if (noAggreg) return rows;//no aggregation, won't do summary
    final IntRead[] aggRows = null == groupRows ? new IntRead[]{rows} : groupRows;
    final int groups = aggRows.length;
    final ForGroup.C[] aggCols = groupAggreg.values().toArray(new ForGroup.C[aggregCnt]);
    final Map<Col, Collection[]> aggMap = new IdentityHashMap<Col, Collection[]>(aggregCnt);
    for (final ForGroup.C col : aggCols) col.submit(true, aggMap);
    final ForGroup.Summary[] summaries = ForGroup.Summary.getInstance(aggMap, aggRows);
    for (int group = 0; group < groups; group++) {//for each group
      final IntRead aggRow = aggRows[group];
      for (int i = 0, m = aggRow.size(); i < m; i++) {//for each row
        final int row = aggRow.get(i);
        for (final ForGroup.Summary sum : summaries) sum.sum(group, row);
      }
    }
    for (final ForGroup.Summary sum : summaries) sum.done();
    for (final ForGroup.C col : aggCols) col.submit(false, aggMap);
    for (int i = 0, n = select.length; i < n; i++)//for solving "isAggregate exposure" problem
      if (select[i].isAggregate()) select[i] = new ColWrap(select[i]);
    return IntRead.nSize(groups);
  }

  void putAliasCols(Map<String, String> aliasName, String table, String column) {
    if (null == table) for (final String name : getDefaultCols()) aliasName.put(name, name);
    else {
      final Map<String, Col> colMap = sourceCol.get(table);
      if (null == colMap) throw needSource(table);
      final String aliasDot = table + ".";
      for (final String name : colMap.keySet()) {
        final String full = aliasDot.concat(name);
        aliasName.put(full, full);
      }
    }
  }
}

final class Sort implements SortInt.Comparator {
  private final int n;
  private final Col[] col;
  private final boolean[] desc;

  Sort(Col[] col, boolean[] desc) {
    this.n = col.length;
    this.col = col;
    this.desc = desc;
  }

  public int compare(int a, int b) {
    for (int i = 0, n = this.n, result; i < n; i++)
      if (0 != (result = col[i].compare(a, b))) return desc[i] ? result > 0 ? -1 : 1 : result;
    return 0;
  }
}

final class MultiMap<K, V> {
  private final Map<K, Collection<V>> map;
  private final boolean uniqueValue;

  MultiMap(Map<K, Collection<V>> map, boolean uniqueValue) {
    this.map = map;
    this.uniqueValue = uniqueValue;
  }

  private Collection<V> newValues() {
    return uniqueValue ? new LinkedHashSet<V>() : new ArrayList<V>();
  }

  public Collection<V> get(K key) {
    return map.get(key);
  }

  public Collection<V> put(K key, V value) {
    Collection<V> c = map.get(key);
    if (null == c) map.put(key, c = newValues());
    c.add(value);
    return c;
  }

  @SuppressWarnings("unchecked")
  public final Collection<V> put(K key, V... value) {
    Collection<V> c = map.get(key);
    if (null == c) map.put(key, c = newValues());
    switch (value.length) {
      case 1:
        c.add(value[0]);
      case 0:
        break;
      default:
        c.addAll(Arrays.asList(value));
    }
    return c;
  }

  public Collection<V> remove(K key) {
    return map.remove(key);
  }

  public void clear() {
    map.clear();
  }

  @Override
  public String toString() {
    return map.toString();
  }
}